/*
Copyright (c) 2010 Justin Ludwig

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/

/**
 * @file jst.sh Templating code.
 */

/**
 * @class JST
 * Main templating class.
 *
 * @constuctor JST
 * @param {optional} o Config properties (copied to this).
 */
function JST(o) {
    // copy properties from o
    if (o)
        for (var i in o)
            this[i] = o[i];
    
    return this;
}

/**
 * @ifunction {static string} JST.DirectiveHandler
 * @param {string} name Directive name.
 * @param {string[string]} attrs Directive attributes.
 * @return The replacement text generated by the directive.
 *
 * @property {static JST.DirectiveHandler[string]} directors Map of directive
 * names to directive handlers.
 */
JST.directors = {};

/**
 * @property {string} template The jst template text.
 * @property {string} code The processed template javascript.
 * @property {function} fn The built template function.
 * @property {string[]} args The argument names for the template function.
 */

/**
 * @function {string} run Runs the templating process.
 * @param {optional string} s Template content.
 * @param {optional object[]} args Execution arguments
 * (or a map of argument names to argument values).
 * @return String generated by executing the template.
 */
JST.prototype.run = function(s, args) {
    // reset template
    if (s) {
        this.template = s;
        delete this.fn;
    }
    
    // rebuild function
    if (!this.fn)
        this.fn = this.build(this.template, args);
    
    // execute function
    return this.execute(args);
}

/**
 * @function {string} execute Executes this pre-built template.
 * @param {optional object[]} args Execution arguments
 * (or a map of argument names to argument values).
 * @return String generated by executing the template.
 */
JST.prototype.execute = function(args) {
    return this.executeAs(this, args);
}

/**
 * @function {string} execute Executes this pre-built template
 * against the specified object.
 * @param {optional} o Object to execute this template against.
 * @param {optional object[]} args Execution arguments
 * (or a map of argument names to argument values).
 * @return String generated by executing the template.
 */
JST.prototype.executeAs = function(o, args) {
    o = o || this;
    args = args || [];

    // extract property values from map
    if (args.constructor != Array)
        args = Utl.propertyValues(args);

    return this.fn.apply(o, args);
}

/**
 * @function {function} build Builds this template
 * into an executable function.
 * @param {optional string} s Template content.
 * @param {optional string[]} args Function argument names
 * (or a map of argument names to argument values).
 * @return Built template.
 */
JST.prototype.build = function(s, args) {
    this.template = s || this.template;
    
    // extract property names from map
    if (args && args.constructor != Array)
        args = Utl.propertyNames(args);
    this.args = args || this.args;

    // build function
    this.fn = eval(this.wrap(this.parse(this.template), this.args));
    return this.fn;
}

/**
 * @function {string} wrap Wraps the processed template content
 * with the boilerplate template-builder header and footer.
 * @param {string} s Processed template content.
 * @param {optional string[]} args Template function argument names.
 * @return Wrapped content.
 */
JST.prototype.wrap = function(s, args) {
    var a = [];
    a.push("(function(");
    if (args)
        for (var i = 0; i < args.length; i++) {
            if (i > 0) a.push(",");
            a.push(args[i]);
        }
    a.push("){\nvar __m=this,__a=[];\n");
    a.push("function out(x){__a.push(String(x));}\n");
    a.push("var __pageUrl='';\n");
    a.push("function relUrl(x){return Utl.relUrl(__pageUrl,x);}\n");
    a.push(s || "");
    a.push("\nreturn __a.join(\"\");\n})");
    return a.join("");
}

/**
 * @function {string} parse Processes the jst template code into js.
 * @param {string} s Template content.
 * @return Processed template content.
 */
JST.prototype.parse = function(s) {
    s = s || "";
    // strip comments
    s = s.replace(/<%--[^\v]*?--%>/g, "");

    // skip empty string (causes infinite loop in re)
    if (!s) return "";
    
    var directors = this.directors || JST.directors;
    var a = [], lastIndex = 0, type = "";
    // parse <%x %> tags
    for (var re = /(?:^|%>)([^\v]*?)(?:<%([@!=])?|$)/g, r; r = re.exec(s);) {
        // tag content
        var js = Utl.trim(s.substring(lastIndex, r.index));

        // handle tag end
        switch (type) {
            case "@": a.push(this.parseDirective(js)); break;
            case "=": a.push(js); a.push(");"); break;
            default: a.push(js); break;
        }

        // print literal content (outside of tags)
        a.push("\nout(\"");
        a.push(Utl.escapeJS(r[1]));
        a.push("\");\n");

        // index of next tag content start
        lastIndex = r.index + r[0].length;
        // type of next tag
        type = r[2] || "";

        // handle tag start
        if (type == "=")
            a.push("out(");
    }

    this.code = a.join("");
    // strip empty out() statements
    this.code = this.code.replace(/out\(("")?\);\n/g, "");
    //Utl.log(Utl.dump(this.code));
    return this.code;
}

/**
 * @function {string} parseDirective Proceses the specified jst directive.
 * @param {string} s Unparsed directive text (ie "page fileVar='file'").
 * @return Directive replacement text.
 */
JST.prototype.parseDirective = function(s) {
    var directors = this.directors || JST.directors;

    // find directive handler
    var directive = String(s.match(/\w+/));
    var director = directors[directive];
    if (!director)
        return "";

    // build directive attribute map
    var attrs = {};
    for (var re = /(\w+)\s*=\s*["']([^"']*)['"]/g, r; r = re.exec(s);)
        attrs[r[1]] = r[2];

    // invoke directive handler
    return director.call(this, directive, attrs);
}

/**
 * @function {static string} pageDirector
 * Handles <code>page</code> directives.
 * @implements DirectiveHandler
 */
JST.directors.page = JST.pageDirector = function(n, o) {
    var file = o.fileVar;
    if (!file)
        file = "\"" + Utl.escapeJS(o.file) + "\"";
    
    return [
        "__pageUrl=", file, ";",
        "out(\"\\n==========\");out(", file, ");out(\"==========\\n\");"
    ].join("");
}

